package org.gpp.proj1.bone;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.scene.Node;
import com.jme.util.LoggingSystem;

public class Bone extends Node {
	
	private static final long serialVersionUID = 6305654273817474690L;

	/**
	 * The bone's child bones
	 */
	private List<Bone>  boneChildren;
	
	/**
	 * The keyframe quaternions
	 */
	protected List<Quaternion> keyframeAngles;
	
	/**
	 * The keyframe length, in update ticks
	 */
	protected List<Float> keyframeLengths;
	
	/**
	 * The keyframe bone widths, in update ticks
	 */
	protected List<Float> keyframeBoneWidths;
	
	/**
	 * The animation map
	 */
	protected Map<String, KeyframeInterval> animations;
	
	/**
	 * The current animation 
	 */
	protected KeyframeInterval actualAnimation;
	
	/**
	 * The current keyframe number
	 */
	protected int actualKeyframeNumber;
	
	/**
	 * The current keyframe length, in ticks
	 */
	protected float actualKeyframeLength;
	
	/**
	 * The ticks passed for the actual keyframe
	 */
	protected float elapsedTicks;

	/**
	 * The actual keyframe's quaternion
	 */
	private Quaternion actualKeyframeQuaternion;
	
	/**
	 * The actual keyframe's bone width
	 */
	private float actualKeyframeWidth;

	/**
	 * The next keyframe's quaternion
	 */
	private Quaternion nextKeyframeQuaternion;
	
	/**
	 * The next keyframe's bone width
	 */
	private float nextKeyframeWidth;
	
	/**
	 * The bone's end joint
	 */
	private Node endJoint;
		
	/**
	 * Default constructor
	 * @param name The node's name
	 */
	public Bone( String name ) {

		this( name, null );
	}
	
	/**
	 * Constructor that reads the keyframes from a file
	 * @param name
	 * @param keyframeFile
	 */
	public Bone( String name, String keyframeFile ) {
		
		super( name );
		
		this.setLocalRotation( new Quaternion() );
		this.boneChildren = new LinkedList<Bone>();
		
		this.keyframeAngles = new LinkedList<Quaternion>();
		this.keyframeLengths = new LinkedList<Float>();
		this.keyframeBoneWidths = new LinkedList<Float>(); 
		
		this.animations = new HashMap<String, KeyframeInterval>();
		
		this.actualAnimation = null;

		if( keyframeFile != null ) {
			this.loadKeyframes( keyframeFile );
		}
		
		this.endJoint = new Node( name + "endJoint" );
		super.attachChild( endJoint );
	}
	
	/**
	 * Adds an animation to the animations map
	 * 
	 * @param animationName The animation's name
	 * @param interval The keyframe interval that represents the animation
	 */
	private void addAnimation( String animationName, KeyframeInterval interval ) {
		assert( animationName != null && animationName.trim().length() > 0 ) : "Animation name cannot be null or empty";
		assert( interval != null ) : "Cannot  set a null interval";
		assert( interval.begin >= 0 && interval.end < keyframeAngles.size() ) : "Interval should be in [0,numberOfKeyframes)";
		assert( interval.begin <= interval.end ) : "For the interval [a,b], a <= b";
		
		this.animations.put( animationName, interval );
	}

	/**
	 * Sets the node's current animation to the 
	 * 
	 * @param animationName
	 */
	public void setAnimation( String animationName ) {
		assert( animations.containsKey( animationName ) ) : "Cannot set a non-existing animation";

		if( animations.containsKey( animationName ) ) {
			this.actualAnimation = animations.get( animationName );

			this.actualKeyframeNumber = actualAnimation.begin - 1;
		
			this.fetchNextKeyframe();
		
			this.elapsedTicks = 0;
		}
		
		for( Bone child : boneChildren ) {
			child.setAnimation( animationName );
		}	
	}
	
	/**
	 * Update the bone''s animation
	 * @param timePerFrame The time passed since the last update, in seconds 
	 */
	public void updateAnimation( float timePerFrame ) {
		
		if( actualAnimation != null ) {
			elapsedTicks += timePerFrame;		
			
			if( elapsedTicks >= this.actualKeyframeLength ) {
				elapsedTicks -= this.actualKeyframeLength;
				this.fetchNextKeyframe();
			}
			
			this.localRotation.set( actualKeyframeQuaternion.x, actualKeyframeQuaternion.y, actualKeyframeQuaternion.z, actualKeyframeQuaternion.w );
			this.localRotation.slerp( nextKeyframeQuaternion, (float)elapsedTicks / (float)this.actualKeyframeLength );
			this.localRotation.normalize();
			
			float deltaWidth = (nextKeyframeWidth - actualKeyframeWidth) / actualKeyframeLength * elapsedTicks;
			this.endJoint.setLocalTranslation( new Vector3f( 0, actualKeyframeWidth + deltaWidth, 0 ) );
			endJoint.updateGeometricState( 0.0f, true );
			
			this.updateGeometricState( 0.0f, true );
		}
		
		for( Bone child : boneChildren ) {
			child.updateAnimation( timePerFrame );
		}
	}
	
	/**
	 * Loads the next keyframe from the animation
	 */
	private void fetchNextKeyframe() {
		
		this.actualKeyframeNumber = actualKeyframeNumber == actualAnimation.end ?
														actualAnimation.begin :
														actualKeyframeNumber + 1;
		int nextKeyframeNumber = actualKeyframeNumber == actualAnimation.end ?
													actualAnimation.begin :
													actualKeyframeNumber + 1;
		
		this.actualKeyframeQuaternion = keyframeAngles.get( actualKeyframeNumber );
		this.actualKeyframeLength = keyframeLengths.get( actualKeyframeNumber );
		this.actualKeyframeWidth = keyframeBoneWidths.get( actualKeyframeNumber );
		
		this.setLocalRotation( new Quaternion( actualKeyframeQuaternion ) );
		this.endJoint.setLocalTranslation( new Vector3f( 0, actualKeyframeWidth, 0 ) );
		
		this.updateGeometricState( 0.0f, false );
		endJoint.updateGeometricState( 0.0f, false );
		
		this.nextKeyframeQuaternion = keyframeAngles.get( nextKeyframeNumber );
		this.nextKeyframeWidth = keyframeBoneWidths.get( nextKeyframeNumber );
	}
	
	/**
	 * Reads the keyframes from a file. Should be on a separate class, though.
	 * 
	 * @param keyframeFile The path to the keyframe file
	 */
	private void loadKeyframes( String keyframeFile ) {
		
		Scanner  scanner = null;
		try {
			scanner = new Scanner( new FileInputStream( keyframeFile ) );
		} catch (FileNotFoundException e) {
			LoggingSystem.getLogger().warning( "Couldn't find keyframe file: " + keyframeFile );
			LoggingSystem.getLogger().throwing( this.getClass().getName(), "loadKeyframes", e );
			
			return;
		}
		
		while( scanner.hasNext() ) {
			String rawLine = scanner.nextLine();
			
			if( rawLine.startsWith( "//" ) || rawLine.startsWith( "#" ) ) {
				continue;
			}
			
			String[] line = rawLine.trim().split( " +" );
			
			if( line.length == 7 ) {
				if( line[0].equals( "keyframe" ) && line[1].equals( this.getName() ) ) {
					float[] angles = { (float)Math.toRadians( Float.parseFloat( line[2] ) ),
												(float)Math.toRadians( Float.parseFloat( line[3] ) ),
												(float)Math.toRadians( Float.parseFloat( line[4] ) ) };				
					Quaternion keyframeAngle = new Quaternion( angles );
					keyframeAngle.normalize();
					
					float keyframeLength = Float.parseFloat( line[5] );
					float keyframeWidth = Float.parseFloat( line[6] );
					
					this.keyframeAngles.add( keyframeAngle );
					this.keyframeLengths.add( keyframeLength );
					this.keyframeBoneWidths.add( keyframeWidth );
				}
			} else if( line.length == 5 ) {
				if( line[0].equals( "animation" ) && line[1].equals( this.getName() ) ) {
					this.addAnimation( line[2], new KeyframeInterval( Integer.parseInt( line[3] ), Integer.parseInt( line[4] ) ) );
				}
			}
		}
	}
	
	/**
	 * Overloads the super class method to handle the bone children
	 * @param child
	 */
	public void attachChild( Bone child ) {
		this.endJoint.attachChild( child );
		this.boneChildren.add( child );		
	}
	
	public boolean isChild(Bone bone){
				
		return this.boneChildren.contains(bone);
	}
}
